#include <Material.hpp>
#include <cstdlib>
#include <glm/gtc/random.hpp>

static float random_float() { return rand() / (RAND_MAX + 1.0); }

static bool near_zero(const glm::vec3& vec) {
  const float s = 1e-8;

  return (glm::abs(vec[0]) < s) && (glm::abs(vec[1]) < s) &&
         (glm::abs(vec[2]) < s);
}

static glm::vec3 reflect(const glm::vec3& v, const glm::vec3& n) {
  return v - 2.f * glm::dot(v, n) * n;
}

static float reflectance(float cosine, float ref_idx) {
  // Use Shlick's approximation for reflectance.
  float r0 = (1.f - ref_idx) / (1.f + ref_idx);
  r0 = r0 * r0;
  return r0 + (1.f - r0) * glm::pow(1.f - cosine, 5.f);
}

bool Lambertian::Scatter(const Ray& ray, const HitRecord& rec,
                         glm::vec3& attenuation, Ray& scattered) const {
  glm::vec3 scatter_direction = rec.normal + glm::sphericalRand(1.f);

  // Catch degenerate scatter direction
  if (near_zero(scatter_direction)) {
    scatter_direction = rec.normal;
  }

  scattered = Ray{rec.p, scatter_direction};
  attenuation = mAlbedo;
  return true;
}

bool Metal::Scatter(const Ray& ray, const HitRecord& rec,
                    glm::vec3& attenuation, Ray& scattered) const {
  glm::vec3 reflected = reflect(glm::normalize(ray.Direction()), rec.normal);
  scattered = Ray(rec.p, reflected + mFuzz * glm::sphericalRand(1.f));

  attenuation = mAlbedo;
  return glm::dot(scattered.Direction(), rec.normal) > 0.f;
}

bool Dielectric::Scatter(const Ray& ray, const HitRecord& rec,
                         glm::vec3& attenuation, Ray& scattered) const {
  attenuation = {1.f, 1.f, 1.f};
  float refraction_ratio = rec.front ? (1.f / mIR) : mIR;

  glm::vec3 unit_direction = glm::normalize(ray.Direction());

  float cos_theta = glm::min(glm::dot(-unit_direction, rec.normal), 1.f);
  float sin_theta = glm::sqrt(1.f - cos_theta * cos_theta);

  bool cannot_refrect = refraction_ratio * sin_theta > 1.f;

  glm::vec3 direction;

  if (cannot_refrect ||
      reflectance(cos_theta, refraction_ratio) > random_float()) {
    direction = reflect(unit_direction, rec.normal);
  } else {
    direction = Ray::Refract(unit_direction, rec.normal, refraction_ratio);
  }

  scattered = Ray(rec.p, direction);

  return true;
}
